﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.Xml.Serialization;

namespace Renci.Data.Interop.Serialization
{
    public partial class InteropSerializer
    {
        private static object _lock = new object();

        internal class TypeAttribute
        {
            public Type Type { get; set; }
            public XmlTypeAttribute Attribute { get; set; }
        }

        /// <summary>
        /// type that this serializer will handle
        /// </summary>
        private Type _type;

        private static ICollection<TypeAttribute> _typeAttributes;
        /// <summary>
        /// Gets the collection of type attributes.
        /// </summary>
        /// <value>The type attributes.</value>
        internal static ICollection<TypeAttribute> TypeAttributes
        {
            get
            {
                if (_typeAttributes == null)
                {
                    lock (_lock)
                    {
                        if (_typeAttributes == null)
                        {
                            var typesMap = (from a in
                                                (from type in Assembly.GetExecutingAssembly().GetTypes()
                                                 select new
                                                 {
                                                     Type = type,
                                                     TypeAttribute = type.GetCustomAttributes(true).OfType<XmlTypeAttribute>().SingleOrDefault(),
                                                 })
                                            where a.TypeAttribute != null
                                            select new TypeAttribute
                                            {
                                                Type = a.Type,
                                                Attribute = new XmlTypeAttribute
                                                {
                                                    AnonymousType = a.TypeAttribute.AnonymousType,
                                                    IncludeInSchema = a.TypeAttribute.IncludeInSchema,
                                                    Namespace = a.TypeAttribute.Namespace,
                                                    TypeName = (string.IsNullOrEmpty(a.TypeAttribute.TypeName)) ? a.Type.Name : a.TypeAttribute.TypeName,
                                                },
                                            });
                            _typeAttributes = new Collection<TypeAttribute>(typesMap.ToList());
                        }
                    }
                }
                return _typeAttributes;

            }
        }

        /// <summary>
        /// 
        /// </summary>
        private static Dictionary<Type, Collection<PropertySerializationInfo>> _propertiesToAttributesMap = new Dictionary<Type, Collection<PropertySerializationInfo>>();

        #region Serilizer methods

        public InteropSerializer()
        {

        }

        public InteropSerializer(Type type)
        {
            this._type = type;
        }

        //
        // Summary:
        //     Serializes the specified System.Object and writes the XML document to a file
        //     using the specified System.IO.Stream.
        //
        // Parameters:
        //   stream:
        //     The System.IO.Stream used to write the XML document.
        //
        //   o:
        //     The System.Object to serialize.
        //
        // Exceptions:
        //   System.InvalidOperationException:
        //     An error occurred during serialization. The original exception is available
        //     using the System.Exception.InnerException property.
        public void Serialize<T>(Stream stream, T o)
        {
            InteropSerializer.WriteXml(XmlWriter.Create(stream), o);
        }

        //
        // summary:
        //     serializes the specified system.object and writes the xml document to a file
        //     using the specified system.io.textwriter.
        //
        // parameters:
        //   textwriter:
        //     the system.io.textwriter used to write the xml document.
        //
        //   o:
        //     the system.object to serialize.
        public void Serialize<T>(TextWriter textwriter, T o)
        {
            InteropSerializer.WriteXml(XmlWriter.Create(textwriter), o);
        }

        //
        // Summary:
        //     Serializes the specified System.Object and writes the XML document to a file
        //     using the specified System.Xml.XmlWriter.
        //
        // Parameters:
        //   xmlWriter:
        //     The System.xml.XmlWriter used to write the XML document.
        //
        //   o:
        //     The System.Object to serialize.
        //
        // Exceptions:
        //   System.InvalidOperationException:
        //     An error occurred during serialization. The original exception is available
        //     using the System.Exception.InnerException property.
        public void Serialize<T>(XmlWriter xmlWriter, T o)
        {
            InteropSerializer.WriteXml(xmlWriter, o);
        }

        ////
        //// Summary:
        ////     Serializes the specified System.Object and writes the XML document to a file
        ////     using the specified System.IO.Streamthat references the specified namespaces.
        ////
        //// Parameters:
        ////   stream:
        ////     The System.IO.Stream used to write the XML document.
        ////
        ////   o:
        ////     The System.Object to serialize.
        ////
        ////   namespaces:
        ////     The System.Xml.Serialization.XmlSerializerNamespaces referenced by the object.
        ////
        //// Exceptions:
        ////   System.InvalidOperationException:
        ////     An error occurred during serialization. The original exception is available
        ////     using the System.Exception.InnerException property.
        //public void Serialize(Stream stream, object o, XmlSerializerNamespaces namespaces)
        //{
        //}

        ////
        //// Summary:
        ////     Serializes the specified System.Object and writes the XML document to a file
        ////     using the specified System.IO.TextWriter and references the specified namespaces.
        ////
        //// Parameters:
        ////   textWriter:
        ////     The System.IO.TextWriter used to write the XML document.
        ////
        ////   o:
        ////     The System.Object to serialize.
        ////
        ////   namespaces:
        ////     The System.Xml.Serialization.XmlSerializerNamespaces that contains namespaces
        ////     for the generated XML document.
        ////
        //// Exceptions:
        ////   System.InvalidOperationException:
        ////     An error occurred during serialization. The original exception is available
        ////     using the System.Exception.InnerException property.
        //public void Serialize(TextWriter textWriter, object o, XmlSerializerNamespaces namespaces)
        //{
        //}

        ////
        //// Summary:
        ////     Serializes the specified System.Object and writes the XML document to a file
        ////     using the specified System.Xml.XmlWriter and references the specified namespaces.
        ////
        //// Parameters:
        ////   xmlWriter:
        ////     The System.xml.XmlWriter used to write the XML document.
        ////
        ////   o:
        ////     The System.Object to serialize.
        ////
        ////   namespaces:
        ////     The System.Xml.Serialization.XmlSerializerNamespaces referenced by the object.
        ////
        //// Exceptions:
        ////   System.InvalidOperationException:
        ////     An error occurred during serialization. The original exception is available
        ////     using the System.Exception.InnerException property.
        //public void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces)
        //{
        //}

        ////
        //// Summary:
        ////     Serializes the specified object and writes the XML document to a file using
        ////     the specified System.Xml.XmlWriter and references the specified namespaces
        ////     and encoding style.
        ////
        //// Parameters:
        ////   xmlWriter:
        ////     The System.xml.XmlWriter used to write the XML document.
        ////
        ////   o:
        ////     The object to serialize.
        ////
        ////   namespaces:
        ////     The System.Xml.Serialization.XmlSerializerNamespaces referenced by the object.
        ////
        ////   encodingStyle:
        ////     The encoding style of the serialized XML.
        ////
        //// Exceptions:
        ////   System.InvalidOperationException:
        ////     An error occurred during serialization. The original exception is available
        ////     using the System.Exception.InnerException property.
        //public void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces, string encodingStyle)
        //{
        //}

        ////
        //// Summary:
        ////     Serializes the specified System.Object and writes the XML document to a file
        ////     using the specified System.Xml.XmlWriter, XML namespaces, and encoding.
        ////
        //// Parameters:
        ////   xmlWriter:
        ////     The System.Xml.XmlWriter used to write the XML document.
        ////
        ////   o:
        ////     The object to serialize.
        ////
        ////   namespaces:
        ////     An instance of the XmlSerializaerNamespaces that contains namespaces and
        ////     prefixes to use.
        ////
        ////   encodingStyle:
        ////     The encoding used in the document.
        ////
        ////   id:
        ////     For SOAP encoded messages, the base used to generate id attributes.
        //public void Serialize(XmlWriter xmlWriter, object o, XmlSerializerNamespaces namespaces, string encodingStyle, string id)
        //{
        //}

        //
        // Summary:
        //     Deserializes the XML document contained by the specified System.IO.Stream.
        //
        // Parameters:
        //   stream:
        //     The System.IO.Stream that contains the XML document to deserialize.
        //
        // Returns:
        //     The System.Object being deserialized.
        public T Deserialize<T>(Stream stream) where T : new()
        {
            T result = new T();
            InteropSerializer.ReadXml(XmlReader.Create(stream), result);
            return result;
        }

        //
        // Summary:
        //     Deserializes the XML document contained by the specified System.IO.TextReader.
        //
        // Parameters:
        //   textReader:
        //     The System.IO.TextReader that contains the XML document to deserialize.
        //
        // Returns:
        //     The System.Object being deserialized.
        //
        // Exceptions:
        //   System.InvalidOperationException:
        //     An error occurred during deserialization. The original exception is available
        //     using the System.Exception.InnerException property.
        public T Deserialize<T>(TextReader textReader) where T : new()
        {
            T result = new T();
            InteropSerializer.ReadXml(XmlReader.Create(textReader), result);
            return result;
        }

        //
        // Summary:
        //     Deserializes the XML document contained by the specified System.xml.XmlReader.
        //
        // Parameters:
        //   xmlReader:
        //     The System.xml.XmlReader that contains the XML document to deserialize.
        //
        // Returns:
        //     The System.Object being deserialized.
        //
        // Exceptions:
        //   System.InvalidOperationException:
        //     An error occurred during deserialization. The original exception is available
        //     using the System.Exception.InnerException property.
        public T Deserialize<T>(XmlReader xmlReader) where T : new()
        {
            T result = new T();
            InteropSerializer.ReadXml(xmlReader, result);
            return result;
        }

        //
        // Summary:
        //     Deserializes the XML document contained by the specified System.IO.Stream.
        //
        // Parameters:
        //   stream:
        //     The System.IO.Stream that contains the XML document to deserialize.
        //
        // Returns:
        //     The System.Object being deserialized.
        public object Deserialize(Stream stream)
        {
            var result = Activator.CreateInstance(this._type);

            InteropSerializer.ReadXml(XmlReader.Create(stream), result);
            return result;
        }

        public object Deserialize(TextReader textReader)
        {
            var result = Activator.CreateInstance(this._type);

            InteropSerializer.ReadXml(XmlReader.Create(textReader), result);
            return result;
        }

        public object Deserialize(XmlReader xmlReader)
        {
            var result = Activator.CreateInstance(this._type);

            InteropSerializer.ReadXml(xmlReader, result);
            return result;
        }

        ////
        //// Summary:
        ////     Deserializes the XML document contained by the specified System.xml.XmlReader
        ////     and encoding style.
        ////
        //// Parameters:
        ////   xmlReader:
        ////     The System.xml.XmlReader that contains the XML document to deserialize.
        ////
        ////   encodingStyle:
        ////     The encoding style of the serialized XML.
        ////
        //// Returns:
        ////     The deserialized object.
        ////
        //// Exceptions:
        ////   System.InvalidOperationException:
        ////     An error occurred during deserialization. The original exception is available
        ////     using the System.Exception.InnerException property.
        //public object Deserialize(XmlReader xmlReader, string encodingStyle)
        //{
        //    return null;
        //}

        ////
        //// Summary:
        ////     Deserializes an XML document contained by the specified System.Xml.XmlReader
        ////     and allows the overriding of events that occur during deserialization.
        ////
        //// Parameters:
        ////   xmlReader:
        ////     The System.Xml.XmlReader that contains the document to deserialize.
        ////
        ////   events:
        ////     An instance of the System.Xml.Serialization.XmlDeserializationEvents class.
        ////
        //// Returns:
        ////     The System.Object being deserialized.
        //public object Deserialize(XmlReader xmlReader, XmlDeserializationEvents events)
        //{
        //    return null;
        //}

        ////
        //// Summary:
        ////     Deserializes the object using the data contained by the specified System.Xml.XmlReader.
        ////
        //// Parameters:
        ////   xmlReader:
        ////     An instance of the System.Xml.XmlReader class used to read the document.
        ////
        ////   encodingStyle:
        ////     The encoding used.
        ////
        ////   events:
        ////     An instance of the System.Xml.Serialization.XmlDeserializationEvents class.
        ////
        //// Returns:
        ////     The object being deserialized.
        //public object Deserialize(XmlReader xmlReader, string encodingStyle, XmlDeserializationEvents events)
        //{
        //    return null;
        //}

        #endregion

        public static void ReadXml(System.Xml.XmlReader reader, object obj)
        {
            //  TODO:   when xsi:nil is specified for value types it will ignore the attribute, since it should be NULL. Not sure what to do when writing XML
            //          may be to indroduce Nullable types at to handle them approapriatly?

            if (obj is IXmlSerializable)
            {
                do
                {
                    //  Find node element
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        //  Get to the element content
                        if (reader.Read())
                        {
                            (obj as IXmlSerializable).ReadXml(reader);
                        }
                    }

                } while (reader.Read());
            }
            else
            {

                do
                {
                    //  TODO:   If reading a root element then make sure that XmlRoot has the same value as reader
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        HandleReadAttributes(reader, obj);

                        while (reader.Read())
                        {
                            if (reader.NodeType == XmlNodeType.Element)
                            {
                                HandleReadNodeElement(reader, obj);
                            }
                            else if (reader.NodeType == XmlNodeType.Text)
                            {
                                HandleReadTextElement(reader, obj);
                            }
                        }
                    }
                }
                while (reader.Read());
            }
        }

        public static void WriteXml<T>(System.Xml.XmlWriter writer, T obj)
        {
            if (obj == null)
                return;
            bool isRootElement = (writer.WriteState == WriteState.Start);

            if (isRootElement)
            {
                var currentType = obj.GetType();
                var rootAttribute = currentType.GetCustomAttributes(true).OfType<XmlRootAttribute>().SingleOrDefault();

                var localName = currentType.Name;
                var ns = string.Empty;

                if (rootAttribute != null)
                {
                    localName = rootAttribute.ElementName;
                    ns = rootAttribute.Namespace;
                }
                writer.WriteStartElement(localName, ns);
            }

            if (obj is IXmlSerializable)
            {
                (obj as IXmlSerializable).WriteXml(writer);
            }
            else
            {
                //  Get XmlNamespaces map
                HandleWriteNamespaceDeclarations(writer, obj);

                HandleWriteAttributes(writer, obj);

                HandleWriteNodeElements(writer, obj);

                HandleWriteTextElement(writer, obj);
            }

            if (isRootElement)
            {
                writer.WriteEndElement();
                writer.Flush();
            }

        }

        public static System.Xml.Schema.XmlSchema GetSchema(object obj)
        {
            return null;
        }

        /// <summary>
        /// Gets the properties attribute map.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private static ICollection<PropertySerializationInfo> GetPropertiesAttributeMap(Type type)
        {
            if (!_propertiesToAttributesMap.ContainsKey(type))
            {
                lock (_lock)
                {
                    if (!_propertiesToAttributesMap.ContainsKey(type))
                    {
                        var propertiesToAttributesMap = new Collection<PropertySerializationInfo>();

                        //  Get type default namespace
                        var xmlTypeAttribute = type.GetCustomAttributes(true).OfType<XmlTypeAttribute>().SingleOrDefault();

                        foreach (var property in type.GetProperties())
                        {                            
                            //  Get property default namespace
                            var xmlPropertyTypeAttribute = property.DeclaringType.GetCustomAttributes(true).OfType<XmlTypeAttribute>().SingleOrDefault();

                            var defaultNamespace = "http://tempuri.org/";

                            if (xmlPropertyTypeAttribute != null && !string.IsNullOrEmpty(xmlPropertyTypeAttribute.Namespace))
                            {
                                defaultNamespace = xmlPropertyTypeAttribute.Namespace;
                            }
                            else if (xmlTypeAttribute != null && !string.IsNullOrEmpty(xmlTypeAttribute.Namespace))
                            {
                                defaultNamespace = xmlTypeAttribute.Namespace;
                            }

                            var propertySerializationInfo = new PropertySerializationInfo(property);
                            //  Add just property serialization info without any attributes yet
                            propertiesToAttributesMap.Add(propertySerializationInfo);

                            var propertyAttributes = property.GetCustomAttributes(true);

                            //  Get default attribute value
                            var defaultValueAttribute = propertyAttributes.OfType<DefaultValueAttribute>().SingleOrDefault();
                            if (defaultValueAttribute != null)
                            {
                                propertySerializationInfo.DefaultValue = (defaultValueAttribute as DefaultValueAttribute).Value;
                            }

                            var choiceAttribute = propertyAttributes.OfType<XmlChoiceIdentifierAttribute>().SingleOrDefault();
                            if (choiceAttribute != null)
                            {
                                propertySerializationInfo.ChoiceField = type.GetProperty(choiceAttribute.MemberName);
                            }

                            foreach (Attribute propertyAttribute in propertyAttributes)
                            {
                                PropertyAttributeSerializationInfo propertyAttributeSerializationInfo = null;

                                if (propertyAttribute is XmlAnyAttributeAttribute)
                                {
                                    propertyAttributeSerializationInfo = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(),
                                        IsAttribute = true,
                                        IsAnyAttribute = true,
                                    };
                                    propertySerializationInfo.HasAttributes = true;
                                }
                                else if (propertyAttribute is XmlAnyElementAttribute)
                                {
                                    propertyAttributeSerializationInfo = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(),
                                        IsElement = true,
                                        IsAnyElement = true,
                                    };
                                    propertySerializationInfo.HasElements = true;
                                }
                                else if (propertyAttribute is XmlArrayAttribute)
                                {
                                    XmlArrayAttribute xmlAttributeAttribute = propertyAttribute as XmlArrayAttribute;

                                    var name = xmlAttributeAttribute.ElementName;
                                    if (string.IsNullOrEmpty(name))
                                    {
                                        name = string.Format("ArrayOf{0}", property.Name);
                                    }

                                    propertySerializationInfo.ArrayRootAttribute = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(name, xmlAttributeAttribute.Namespace ?? defaultNamespace),
                                        IsElement = true,
                                        Form = xmlAttributeAttribute.Form,
                                        IsNullable = xmlAttributeAttribute.IsNullable,
                                    };
                                    propertySerializationInfo.HasElements = true;

                                }
                                else if (propertyAttribute is XmlArrayItemAttribute)
                                {
                                    XmlArrayItemAttribute xmlAttributeAttribute = propertyAttribute as XmlArrayItemAttribute;

                                    var name = xmlAttributeAttribute.ElementName;
                                    if (string.IsNullOrEmpty(name))
                                    {
                                        name = property.Name;
                                    }

                                    propertyAttributeSerializationInfo = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(name, xmlAttributeAttribute.Namespace ?? defaultNamespace),
                                        Type = xmlAttributeAttribute.Type ?? GetElementType(property.PropertyType),
                                        IsElement = true,
                                        Form = xmlAttributeAttribute.Form,
                                        IsNullable = xmlAttributeAttribute.IsNullable,
                                        IsArrayItem = true,
                                    };
                                    propertySerializationInfo.HasElements = true;
                                }
                                else if (propertyAttribute is XmlAttributeAttribute)
                                {
                                    XmlAttributeAttribute xmlAttributeAttribute = propertyAttribute as XmlAttributeAttribute;

                                    var name = xmlAttributeAttribute.AttributeName;
                                    if (string.IsNullOrEmpty(name))
                                    {
                                        name = property.Name;
                                    }

                                    propertyAttributeSerializationInfo = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(name, xmlAttributeAttribute.Namespace ?? defaultNamespace),
                                        Type = xmlAttributeAttribute.Type ?? GetElementType(property.PropertyType),
                                        IsAttribute = true,
                                        Form = xmlAttributeAttribute.Form,
                                    };
                                    propertySerializationInfo.HasAttributes = true;
                                }
                                else if (propertyAttribute is XmlElementAttribute)
                                {
                                    XmlElementAttribute xmlElementAttribute = propertyAttribute as XmlElementAttribute;
                                    var name = xmlElementAttribute.ElementName;
                                    if (string.IsNullOrEmpty(name))
                                    {
                                        name = property.Name;
                                    }

                                    propertyAttributeSerializationInfo = new PropertyAttributeSerializationInfo(property)
                                    {
                                        Name = new XmlQualifiedName(name, xmlElementAttribute.Namespace ?? defaultNamespace),
                                        Type = xmlElementAttribute.Type ?? GetElementType(property.PropertyType),
                                        IsElement = true,
                                        Form = xmlElementAttribute.Form,
                                        IsNullable = xmlElementAttribute.IsNullable,
                                    };
                                    propertySerializationInfo.HasElements = true;
                                }
                                else if (propertyAttribute is XmlIgnoreAttribute)
                                {
                                    propertySerializationInfo.IsIgnore = true;
                                }
                                else if (propertyAttribute is XmlTextAttribute)
                                {
                                    propertySerializationInfo.IsText = true;
                                }

                                //else if (propertyAttribute is XmlEnumAttribute)
                                //{
                                //}
                                //XmlIncludeAttribute
                                //XmlRootAttribute
                                //XmlTypeAttribute
                                if (propertyAttributeSerializationInfo != null)
                                {
                                    propertyAttributeSerializationInfo.DefaultValue = propertySerializationInfo.DefaultValue;
                                    propertyAttributeSerializationInfo.ChoiceField = propertySerializationInfo.ChoiceField;

                                    propertiesToAttributesMap.Add(propertyAttributeSerializationInfo);
                                }
                            }
                        }

                        _propertiesToAttributesMap.Add(type, propertiesToAttributesMap);

                        return propertiesToAttributesMap;
                    }
                    else
                    {
                        return _propertiesToAttributesMap[type];
                    }
                }
            }
            else
            {
                return _propertiesToAttributesMap[type];
            }
        }

        internal static Type GetElementType(Type type)
        {
            var result = type;
            if (type.GetInterface("ICollection") != null)
            {
                if (type.IsArray)
                {
                    result = type.GetElementType();
                }
                else
                {
                    result = type.GetGenericArguments().SingleOrDefault();
                }

                //  If not generic collection that type should be object
                if (result == null)
                {
                    result = typeof(object);
                }
            }
            return result;
        }
    }
}
